﻿using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace ooWii.IO
{
	public class VolumeStream : Stream
	{
		#region VolumeStream

		SafeFileHandle handle;
		long position;
		long length;

		int sectorSize;
		byte[] sectorBuffer;
		int sectorBufferAlignedOffset;
		IntPtr sectorBufferAlignedPtr;
		GCHandle sectorBufferHandle;
		long sectorPosition = -1;
		int maxBytes;
		int bytesRead;

		public VolumeStream( string volume )
		{
			handle = CreateFile( volume, GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, IntPtr.Zero );
			if( handle.IsInvalid )
			{
				Marshal.ThrowExceptionForHR( Marshal.GetHRForLastWin32Error() );
			}

			DISK_GEOMETRY diskGeometry;
			if( !deviceIoGetOutput<DISK_GEOMETRY>( handle, IOCTL_DISK_GET_DRIVE_GEOMETRY, out diskGeometry ) )
			{
				Marshal.ThrowExceptionForHR( Marshal.GetHRForLastWin32Error() );
			}

			sectorSize = (int)diskGeometry.BytesPerSector;
			length = (long)( diskGeometry.Cylinders * diskGeometry.TracksPerCylinder * diskGeometry.SectorsPerTrack * diskGeometry.BytesPerSector );

			GET_LENGTH_INFORMATION lengthInformation;
			if( deviceIoGetOutput<GET_LENGTH_INFORMATION>( handle, IOCTL_DISK_GET_LENGTH_INFO, out lengthInformation ) )
			{
				length = lengthInformation.Length;
			}

			maxBytes = WiiBuffer.Align( 32768, sectorSize );

			sectorBuffer = new byte[ maxBytes + sectorSize ];

			sectorBufferHandle = GCHandle.Alloc( sectorBuffer, GCHandleType.Pinned );
			IntPtr bufferAddress = sectorBufferHandle.AddrOfPinnedObject();

			sectorBufferAlignedOffset = sectorSize - (int)( bufferAddress.ToInt64() & ( sectorSize - 1 ) );

			sectorBufferAlignedPtr = Marshal.UnsafeAddrOfPinnedArrayElement( sectorBuffer, sectorBufferAlignedOffset );
		}

		bool deviceIoGetOutput<T>( SafeFileHandle handle, UInt32 ioControlCode, out T output )
		{

			int bufferSize = Marshal.SizeOf( typeof( T ) );
			IntPtr buffer = Marshal.AllocHGlobal( bufferSize );

			try
			{
				Int32 bytesRead = 0;

				if( !DeviceIoControl( handle, ioControlCode, IntPtr.Zero, 0, buffer, bufferSize, out bytesRead, IntPtr.Zero ) )
				{
					output = default( T );
					return false;
				}

				if( bytesRead != bufferSize )
				{
					throw new IOException( string.Format( "DeviceIoControl {0} received {1} bytes expected {2} bytes", typeof( T ).Name, bytesRead, bufferSize ) );
				}

				output = (T)Marshal.PtrToStructure( buffer, typeof( T ) );
				return true;
			}
			finally
			{
				Marshal.FreeHGlobal( buffer );
			}
		}

		#endregion

		#region Stream

		public override bool CanRead
		{
			get
			{
				return true;
			}
		}

		public override bool CanSeek
		{
			get
			{
				return true;
			}
		}

		public override bool CanWrite
		{
			get
			{
				return false;
			}
		}

		public override void Flush()
		{
		}

		public override long Length
		{
			get
			{
				return length;
			}
		}

		public override long Position
		{
			get
			{
				return position;
			}
			set
			{
				if( value < 0 )
				{
					throw new ArgumentOutOfRangeException( "value", "Non-negative number required." );
				}

				position = value;
			}
		}

		public override int Read( byte[] buffer, int offset, int count )
		{
			int read = 0;

			if( length - position < count )
			{
				count = (int)( length - position );
			}

			while( count > 0 )
			{
				int seekOffset = (int)( ( position + read ) % sectorSize );
				long seekPosition = ( position + read ) - seekOffset;

				if( sectorPosition != seekPosition )
				{
					int newPosition = (int)( seekPosition & 0xffffffff );
					int newPositionHigh = (int)( seekPosition >> 32 );

					int actualPositionHigh = newPositionHigh;
					UInt32 actualPosition = SetFilePointer( handle, newPosition, ref actualPositionHigh, 0 );
					if( actualPosition == INVALID_SET_FILE_POINTER && Marshal.GetLastWin32Error() != 0 )
					{
						Marshal.ThrowExceptionForHR( Marshal.GetHRForLastWin32Error() );
					}

					if( newPosition != actualPosition || newPositionHigh != actualPositionHigh )
					{
						throw new IOException( "seek failed" );
					}

					int bytesRequested = WiiBuffer.Align( count, sectorSize );
					if( bytesRequested > maxBytes )
					{
						bytesRequested = maxBytes;
					}

					if( !ReadFile( handle, sectorBufferAlignedPtr, bytesRequested, out bytesRead, IntPtr.Zero ) || bytesRead != bytesRequested )
					{
						return 0;
					}

					sectorPosition = seekPosition;
				}

				int available = bytesRead - seekOffset;
				if( available > count )
				{
					available = count;
				}

				Buffer.BlockCopy( sectorBuffer, sectorBufferAlignedOffset + seekOffset, buffer, offset + read, available );

				count -= available;
				read += available;
			}

			position += read;
			return read;
		}

		public override long Seek( long offset, SeekOrigin origin )
		{
			long newPosition;

			switch( origin )
			{
			case SeekOrigin.Begin:
				newPosition = offset;
				break;

			case SeekOrigin.Current:
				newPosition = this.Position + offset;
				break;

			case SeekOrigin.End:
				newPosition = this.Length + offset;
				break;

			default:
				throw new ArgumentException( "origin" );
			}

			if( newPosition < 0 )
			{
				throw new IOException( "An attempt was made to move the file pointer before the beginning of the file." );
			}

			position = newPosition;
			return position;
		}

		public override void SetLength( long value )
		{
			throw new NotSupportedException();
		}

		public override void Write( byte[] buffer, int offset, int count )
		{
			throw new NotSupportedException();
		}

		public override void Close()
		{
			handle.Close();
			sectorBufferHandle.Free();

			base.Close();
		}

		#endregion

		#region Native

		[DllImport( "kernel32.dll", SetLastError = true )]
		static extern SafeFileHandle CreateFile( string lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile );

		[DllImport( "kernel32.dll", SetLastError = true )]
		static extern UInt32 SetFilePointer( SafeHandle hFile, int lDistanceToMove, ref int lpDistanceToMoveHigh, int dwMoveMethod );

		[DllImport( "kernel32", SetLastError = true )]
		[return: MarshalAs( UnmanagedType.Bool )]
		static extern bool ReadFile( SafeHandle hFile, IntPtr pBuffer, Int32 NumberOfBytesToRead, out Int32 pNumberOfBytesRead, IntPtr Overlapped );

		[DllImport( "kernel32.dll", SetLastError = true )]
		static extern UInt32 GetFileSize( SafeHandle hFile, ref UInt32 lpFileSizeHigh );

		[DllImport( "Kernel32.dll", SetLastError = true )]
		[return: MarshalAs( UnmanagedType.Bool )]
		public static extern bool DeviceIoControl( SafeHandle hDevice, UInt32 dwIoControlCode, IntPtr InBuffer, int nInBufferSize, IntPtr OutBuffer, int nOutBufferSize, out int pBytesReturned, IntPtr lpOverlapped );

		[DllImport( "kernel32.dll", SetLastError = true )]
		[return: MarshalAs( UnmanagedType.Bool )]
		static extern bool CloseHandle( SafeHandle hObject );

		const UInt32 GENERIC_READ = 0x80000000;
		const UInt32 FILE_SHARE_READ = 0x00000001;
		const UInt32 OPEN_EXISTING = 3;
		const UInt32 FILE_FLAG_NO_BUFFERING = 0x20000000;

		const UInt32 INVALID_SET_FILE_POINTER = 0xffffffff;

		const UInt32 IOCTL_DISK_GET_DRIVE_GEOMETRY = 0x70000;
		const UInt32 IOCTL_DISK_GET_LENGTH_INFO = 0x7405c;

		#endregion

		struct DISK_GEOMETRY
		{
			#region DISK_GEOMETRY

#pragma warning disable 649

			public UInt64 Cylinders;
			public UInt32 MediaType;
			public UInt32 TracksPerCylinder;
			public UInt32 SectorsPerTrack;
			public UInt32 BytesPerSector;

#pragma warning restore 649

			#endregion
		}

		struct GET_LENGTH_INFORMATION
		{
			#region GET_LENGTH_INFORMATION

#pragma warning disable 649

			public long Length;

#pragma warning restore 649

			#endregion
		}
	}
}
